package objects;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.table.DefaultTableModel;

import tinyTorrent.Chunk;
import tinyTorrent.HashValue;
import tinyTorrent.TinyTorrent;
import utils.FilesUtils;
import utils.MonFiltre;
import client.Client;
import client.RemoteClient;
import client.Tracker;
import exceptions.ChunkNotAvailableException;
import exceptions.FileIsNotDirectory;

/**
 *
 *
 *
 */
public class ClientImpl implements Client,RemoteClient,Serializable {

    /**
     *
     */
    private static final long serialVersionUID = 1L;
    private File sharedFolder;
    private String trackerUrl;
    private Tracker tracker;
    private String identifier;
    private long up =1000000;
    private long down=1000000;
    private String nameClient;

    public ClientImpl(final File f, final String trackerUrl) throws FileIsNotDirectory,RemoteException,MalformedURLException,NotBoundException
    {

        if(f.isDirectory())
        {
            sharedFolder = f;
            this.trackerUrl = trackerUrl;
            tracker = (Tracker) Naming.lookup(this.trackerUrl);
            this.identifier = new BigInteger(130, new SecureRandom()).toString(32);
        } else {
            throw new FileIsNotDirectory(f);
        }
    }

    @Override
    public File share(final File file) {

        System.out.println("###################  Upload share  ###################");
        final String nameClient = sharedFolder.getAbsolutePath();
        final File[] morceaux = FilesUtils.createChunks(file, sharedFolder);
        final Chunk[] chunks  = new Chunk[morceaux.length];
        final HashValue resourceHashValue = new HashValue(file);
        final HashValue[] chunkHashValues = new HashValue[morceaux.length];
        for(int i=0 ; i<morceaux.length ; i++)
        {
            chunks[i] = new Chunk(morceaux[i],i, nameClient);
            chunkHashValues[i] = new HashValue(Chunk.write(chunks[i],sharedFolder));
        }

        System.out.println("pieces chunks : "+file.getName()+" created");

        final TinyTorrent t = new TinyTorrent(trackerUrl, file.getName(), resourceHashValue, chunkHashValues);

        // informe le tracker du nouveau fichier
        try {
            tracker.register(t.getResourceHashValue(), this);
            //tracker.register(t, this);
        }
        catch (final RemoteException e) {e.printStackTrace();}


        return TinyTorrent.write(t, sharedFolder);
    }

    public Set<RemoteClient> getSeeders(final TinyTorrent torrent) throws RemoteException {
        return tracker.findClients(torrent.getResourceHashValue());
    }

    @Override
    public File download(final TinyTorrent torrent, final DefaultTableModel newModel) {
        try {

            System.out.println("###################  Upload download  ###################");
            System.out.println("Start downloading the resource : "+torrent.getResourceName());

            // charche les remotes disposant de la ressource
            final Set<RemoteClient> seeders = getSeeders(torrent);

            if(seeders.size() > 1) {
                System.out.println(seeders.size() + " Seeders were found");
            } else {
                System.out.println(seeders.size() + " Seeder was found");
            }

            // liste des index t�l�charg�
            final List<Integer> indexDispos  = new ArrayList<Integer>();

            // contient la liste des chunks t�l�charg�
            final List<Chunk> chunksdownloaded = downloadChunks(seeders, torrent.getResourceHashValue(), indexDispos, newModel);

            // contiendra la liste des fichiers correspondants aux chunks t�l�charg�s
            final File[] listChunksdownloaded = new File[chunksdownloaded.size()];

            // emplacement du chunk
            final File chunksDir = new File(sharedFolder.getAbsolutePath()+File.separator+torrent.getResourceName()+File.separator+"chunks");
            chunksDir.mkdirs();

            for(int i=0 ; i<listChunksdownloaded.length ; i++)
            {
                listChunksdownloaded[i] = Chunk.write(chunksdownloaded.get(i), chunksDir);
                down += listChunksdownloaded[i].getUsableSpace();
                // test si le chunk t�l�charg� est valide ou non
                boolean test = false;
                for(final HashValue h :torrent.getChunkHashValues())
                {
                    if(h.equals(new HashValue(listChunksdownloaded[i])))
                    {
                        test = true;
                        System.out.println("chunk"+i+" is good.");
                    }
                }
                if(!test) {
                    System.out.println("chunk"+i+" was not downloaded properly");
                }
            }


            // si on a pu t�l�charger tous les chunks on peut les fusionner
            // sinon, il faut attendre, ou au moins notifier l'utilisateur
            if(indexDispos.size() == torrent.getChunkHashValues().length)
            {
                final File downloadedFile = new File(
                    sharedFolder.getAbsolutePath()
                    + File.separator
                    + torrent.getResourceName()
                    + File.separator
                    + torrent.getResourceName()
                );
                FilesUtils.merge(downloadedFile, listChunksdownloaded);

                final File[] files = chunksDir.listFiles();
                for( int i = 0 ; i < files.length ; i++ )
                {
                    files[ i ].setWritable(true);
                    System.out.println("removing pieces chunks in : "+chunksDir+files[i].getName()+" is successful");
                }

                chunksDir.setWritable(true);
                if(!FilesUtils.delete(chunksDir)) {
                    System.out.println("can not delete pieces chunks in : "+chunksDir);
                } else {
                    System.out.println("removing chunk directory : "+chunksDir+" is successful");
                }
                System.out.println(torrent.getResourceName()+" is successfully downloaded !");
                return downloadedFile;
            } else {
                System.out.println("The file cannot be assembled because it lacks parts not downloaded");
            }

        }
        catch (final RemoteException e)                {e.printStackTrace();}
        catch (final IOException e)                    {e.printStackTrace();}
        catch (final ChunkNotAvailableException e)    {e.printStackTrace();} catch (final Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

    /**
     * t�l�charge tous les chunks aupr�s des Seeders disponibles
     * et remplit une liste des index correspondant aux Chunks telecharg�s (titre indicatif)
     * que l'on peut utiliser pour t�l�charger les index manquant apr�s.
     * <br>Cette m�thode applique une strat�gie pour maximiser le nombre des RemoteClient disposants
     * de la ressource demand�
     * @param seeders tous les RemoteClient connect� disposant de la ressource
     * @param resourceHashValue la valeur de hashage de la ressource � t�l�charger
     * @param indexDispos liste qui m�morise les index des Chunks t�l�charg�
     * @return List une liste des Chunks t�l�charg�s
     * @throws RemoteException
     * @throws ChunkNotAvailableException
     */
/*    private List<Chunk> downloadChunks(
        final Set<RemoteClient> seeders,
        final HashValue resourceHashValue,
        final List<Integer> indexDispos
    ) throws RemoteException, ChunkNotAvailableException {
        final List<Integer> indChunks = new ArrayList<Integer>();
        final List<RemoteClient> remoteChunks = new ArrayList<RemoteClient>();

        for(final RemoteClient seeder :seeders)  {
            final Set<Integer> inds = seeder.findAvailableChunks(resourceHashValue);

            System.out.println(
                "Seeder [" + seeder.getIdentifier() + "] avaliable chunks " + inds
            );

            for(final Integer i : inds) {
                // m�morise tous les index et remoteClient trouv�
                remoteChunks.add(seeder);
                indChunks.add(i);
                // on mis � jour la liste finale des index trouv� (index sans r�p�tition)
                if (indexDispos.indexOf(i) == -1) {
                    indexDispos.add(i);
                }
            }
        }

        // on cherche les chunks les plus rares
        doOccurrenceSort(indChunks, remoteChunks);
        // on assure le t�l�chargement depuis les clients disposant de moins de chunks
        doChunksNbrSort(indChunks, remoteChunks, resourceHashValue);

        return leechChunks(indChunks, remoteChunks, resourceHashValue);
    }
*/
    /**
     * New variant
     */
    private List<Chunk> downloadChunks(
        final Set<RemoteClient> seeders,
        final HashValue resourceHashValue,
        final List<Integer> indexDispos,
        final DefaultTableModel newModel

    ) throws RemoteException, ChunkNotAvailableException {
        final List<Integer> indChunks = new ArrayList<Integer>();
        final List<RemoteClient> remoteChunks = new ArrayList<RemoteClient>();

        final Map<Integer, Set<RemoteClient>> chunkMap = new HashMap<Integer, Set<RemoteClient>>();
        Set<RemoteClient> avaliableClient;
        for(final RemoteClient seeder :seeders)  {

            final Set<Integer> inds = seeder.findAvailableChunks(resourceHashValue);
            System.out.println(
                "Seeder [" + seeder.toString() + "] avaliable chunks " + inds
            );

            for(final Integer i : inds) {
                avaliableClient = chunkMap.containsKey(i) ? chunkMap.get(i) : new HashSet();
                avaliableClient.add(seeder);
                chunkMap.put(i, avaliableClient);
            }
        }

        final List<ChunkRate> chunkRates = new ArrayList<ChunkRate>();
        for (final Integer chunk : chunkMap.keySet()) {
            System.out.println("Chunk [" + chunk + "] available in " + chunkMap.get(chunk));
            chunkRates.add(new ChunkRate(chunk, chunkMap.get(chunk)));
        }

        Collections.sort(chunkRates);
        System.out.println("After checking rating");
        final List<Chunk> chunksdownloaded = new ArrayList<Chunk>();

        final int step = chunkRates.size() != 0 ? 100 / chunkRates.size() : 0;
        int cnt = 1;

        for (final ChunkRate chunkRate : chunkRates) {
            final Integer chunk = chunkRate.getChunk();
            final RemoteClient desiredClient = chunkRate.getDesiredClient();
            System.out.println(
                "Chunk [" + chunk + "] available in " + chunkRate.getClients()
                + " desired client " + desiredClient
            );

            final Chunk chunktorrent = desiredClient.leech(resourceHashValue, chunk);
            chunksdownloaded.add(chunktorrent);
            System.out.println(
                "Download chunk " + chunk + " from the " + chunktorrent.getNameClient()
                    + " : " + desiredClient
            );

            if (newModel != null) {
                newModel.setValueAt(step * cnt++, newModel.getRowCount() - 1, 2);
            }

            indexDispos.add(chunkRate.getChunk());
            remoteChunks.add(chunkRate.getDesiredClient());
            indChunks.add(chunkRate.getChunk());
        }

        return leechChunks(indChunks, remoteChunks, resourceHashValue);
    }

    /**
     * Tri la liste indexChunk-remoteChunk par ordre de raret�
     * @param indChunks Liste contenant les index des chunks
     * @param remoteChunks liste contenant les remotes disposant du chunk qui lui correspond dans la liste indChunks
     */
    public void doOccurrenceSort(final List<Integer> indChunks, final List<RemoteClient>remoteChunks)
    {
        for(int i=0 ; i<indChunks.size()-1 ; i++)
        {
            if(Collections.frequency(indChunks, indChunks.get(i))==1) {
                ;
            }
            for(int j=i+1 ; j<indChunks.size() ; j++)
            {
                if(Collections.frequency(indChunks, indChunks.get(i)) > Collections.frequency(indChunks, indChunks.get(j)))
                {
                    // on remonte le chunk le plus rare
                    Collections.swap(indChunks, i, j);
                    Collections.swap(remoteChunks, i, j);
                }
            }
        }
    }


    /**
     * tri le la liste indexChunk-remoteChunk par ordre des clients disposants de moins de ressources
     * <br> afin d'assurer le t�l�chargement du maximum des chunks
     * <br> aussi cette fonction essaye � chaque fois prendre un remoteClient qui n'a pas encore partag�
     * <br> cette approche est tr�s utile lors de l'utilisation des Thread
     * @param indChunks indChunks Liste contenant les index des chunks
     * @param remoteChunks liste contenant les remotes disposant du chunk qui correspond � l'index dans la liste indChunks
     * @param resourceHashValue la valeur de Hashage de la ressource
     * @throws RemoteException
     */
    public void doChunksNbrSort(final List<Integer> indChunks, final List<RemoteClient> remoteChunks, final HashValue resourceHashValue) throws RemoteException
    {
        final List<Integer> iTmp = new ArrayList<Integer>();
        boolean test =true;
        for(int i=0 ; i<indChunks.size()-1 ; i++)
        {
            for(int j=i+1 ; j<indChunks.size() ; j++)
            {
                if(indChunks.get(i).equals(indChunks.get(j)))
                {
                    if(!remoteChunks.get(i).equals(remoteChunks.get(j)))
                    {
                        if(iTmp.indexOf(i) == -1)
                        {
                            if(test)
                            {
                                iTmp.add(i);
                                test=false;
                            }
                            else
                            {
                                iTmp.add(j);
                                test = true;
                            }
                        }
                    }
                }
            }
        }
        // pour eviter tous les probl�me lors de la supprission des index-remote inutiles
        Collections.sort(iTmp);
        Collections.reverse(iTmp);
        for(final Integer i :iTmp)
        {
            // supprission des doublons
            indChunks.remove(i.intValue());
            remoteChunks.remove(i.intValue());
        }
    }


    /**
     * t�l�charge les chunks � partir de la liste pr�-remplit des index et remoteClient
     * <br>et retourne la liste des chunks t�l�charg�s
     * @param indChunks indChunks indChunks Liste contenant les index des chunks
     * @param remoteChunks liste contenant les remotes disposant du chunk qui correspond � l'index dans la liste indChunks
     * @param resourceHashValue
     * @return chunksdownloaded liste des chunks t�l�charg�s
     * @throws RemoteException
     * @throws ChunkNotAvailableException
     */
    public List<Chunk> leechChunks(final List<Integer> indChunks, final List<RemoteClient> remoteChunks, final HashValue resourceHashValue) throws RemoteException, ChunkNotAvailableException
    {
        final List<Chunk> chunksdownloaded = new ArrayList<Chunk>();

//        for(int i=0 ; i<indChunks.size() ; i++) {
        for (int i = 0; i < remoteChunks.size(); i++) {
            chunksdownloaded.add(remoteChunks.get(i).leech(resourceHashValue, indChunks.get(i)));
            System.out.println("download chunk"+indChunks.get(i)+" from the " + chunksdownloaded.get(i).getNameClient() + " : "+remoteChunks.get(i).getIdentifier());
        }
        return chunksdownloaded;
    }



    @Override
    public Chunk leech(final HashValue resourceHashValue, final int chunkIndex) throws RemoteException, ChunkNotAvailableException {
        final FilenameFilter filtre = new MonFiltre(new String[]{"tinytorrent"}, "only tinytorrent files");
        final File[] fichiers = sharedFolder.listFiles(filtre);
        for(final File f :fichiers)
        {
            final TinyTorrent t = TinyTorrent.read(f);
            if(t.getResourceHashValue().equals(resourceHashValue))
            {
                final String nameClient = sharedFolder.getAbsolutePath();
                final File fchunk = new File(nameClient+"/"+t.getResourceName()+".chunk"+chunkIndex);
                if(fchunk.exists())
                {
                    up += fchunk.length();
                    return new Chunk(fchunk, chunkIndex, nameClient);
                }
            }
        }
        return null;
    }

    @Override
    public Set<Integer> findAvailableChunks(final HashValue resourceHashValue) throws RemoteException {

        final Set<Integer> indexDispos = new HashSet<Integer>();
        final File[] fichiers = sharedFolder.listFiles();
        for(final File f :fichiers)
        {
            if(f.canRead() && !f.isDirectory())
            {
                if(f.getName().endsWith("tinytorrent"))
                {
                    final TinyTorrent t = TinyTorrent.read(f);
                    if(t.getResourceHashValue().equals(resourceHashValue))
                    {


                        for(final File fs :fichiers)
                        {
                            if(fs.getName().contains(".chunk"))
                            {
                                final int index = fs.getName().lastIndexOf(".");
                                // contient le nom de la ressource du chunk sans le ".chunk"
                                final String nomChunk = fs.getName().substring(0, index+6);

                                if(nomChunk.equals(t.getResourceName()+".chunk"))
                                {
                                    // contient l'index mais en String
                                    final String indexDispo = fs.getName().substring(index+1, fs.getName().length());
                                    // on extrait l'index
                                    final String resIndex = indexDispo.replace("chunk", "");
                                    final int ind = Integer.parseInt(resIndex);
                                    indexDispos.add(ind);
                                }
                            }

                        }
                        return indexDispos;
                    }
                }
            }
        }
        return indexDispos;
    }

    @Override
    public String getIdentifier() throws RemoteException {

        return this.identifier;
    }

    public String getTrackerUrl() {
        return trackerUrl;
    }

    public void setTrackerUrl(final String trackerUrl) {
        this.trackerUrl = trackerUrl;
    }

    public void setSharedFolder(final File sharedFolder) {
        this.sharedFolder = sharedFolder;
    }

    @Override
    public File getSharedFolder() {
        return sharedFolder;
    }

    @Override
    public Double getRatio() throws RemoteException {

        final Double db1 = new Double(up);
        final Double db2 = new Double(down);

        if(down<=0) {
            return 0.0;
        }

        final Double res = db1/db2;
        return res;
    }

    @Override
    public long getUp() {
        return up;
    }

    @Override
    public long getDown() {
        return down;
    }

    @Override
    public String UptoString() {
        return FilesUtils.getFormattedFileSize(up);
    }

    @Override
    public String DowntoString() {
        return FilesUtils.getFormattedFileSize(down);
    }

    public Tracker getTracker() {
        return tracker;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "identifier="
                + identifier + "";
    }





}

